# @Author :等风的云
# -*- coding: utf-8 -*-
# 朴素贝叶斯模型
import numpy as np


class NBC(object):
    """实现贝叶斯分类器"""

    def __init__(self, laplace_smoothing=1):
        """
        初始化
        :param laplace_smoothing: 拉普拉斯平滑 默认是1
        """
        self.laplace_smoothing = laplace_smoothing
        self.pp = None
        self.cp = None
        self.classes = None

    def _prior_probability(self, y, classes):
        """
        计算先验概率 log(p(y=ck))
        :param y:
        :param classes:
        :return:
        """
        count = np.count_nonzero(y == classes[:, None], axis=1)
        p = (count + self.laplace_smoothing) / (y.size + classes.size * self.laplace_smoothing)
        return np.log(p)

    def _con_probability(self, x, y, classes):
        """
        条件概率 p(x=xi|y=ck)
        :param x:
        :param y:
        :param classes:
        :return:
        """
        # 我们实现的是最简单的伯努利分布所有特征取值只有0 或 1
        _, n = x.shape
        k = classes.size
        cp = np.empty((2, k, n))  # 2表示特征的取值为2个，k表示类别，n表示特征个数
        for i, v in enumerate(classes):
            # 遍历每个类别，求其对应的条件概率
            x_ = x[y == v]
            count = np.count_nonzero(x_, axis=0)  # 统计特征值为1的数目
            p = (count + self.laplace_smoothing) / (len(x_) + 2 * self.laplace_smoothing)
            cp[1, i] = p
            cp[0, i] = 1 - p
        return np.log(cp)

    def train(self, x_train, y_train):
        """
        训练数据
        :param x_train:
        :param y_train:
        :return:
        """
        # 统计类别
        self.classes = np.unique(y_train)
        self.pp = self._prior_probability(y_train, self.classes)
        self.cp = self._con_probability(x_train, y_train, self.classes)

    def _predict_one(self, x):
        """
        单个样本预测
        :param x:
        :return:
        """
        """
        带入假设函数 求解后验概率
         log(p(y=ck)) + ∑(log(p(x=xi|y=ck)))
        """
        k = self.classes.size
        p = np.empty(k)  # 存储每个类别的后验概率
        idx_0 = x == 0
        idx_1 = x == 1
        for i in range(k):
            p[i] = self.pp[i] + np.sum(self.cp[0, i][idx_0]) + np.sum(self.cp[1, i][idx_1])
        return np.argmax(p)

    def predict(self, x):
        """
        批量预测
        :param x:
        :return:
        """
        return np.apply_along_axis(self._predict_one, 1, x)


if __name__ == '__main__':
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split

    data = np.genfromtxt('nbc_spambase.data', delimiter=",")
    # 该数据有0-57列 0-47 代表48个不同单词出现的频数， 48-53表示标点符号频数 54-56代表连续大写字母特征 57代表类别
    # 我们的模型是伯努利模型（二元分类）所以 我们只考虑 出现或者未出现  不考虑频数的不同, 如果考虑将频数分组，那么就变为多分类
    # 所以我们将数据进行转化为0-1型 出现为1 不出现为0
    x = data[:, : 48]
    y = data[:, -1].astype(np.int)
    X = np.where(x > 0, 1, 0)
    X_train, X_test, Y_train, Y_test = train_test_split(X, y, test_size=0.9)
    nbc = NBC()
    nbc.train(X_train, Y_train)
    y_predict = nbc.predict(X_test)
    print("真值：\n", Y_test)
    print("预测值：\n", y_predict)
    print(accuracy_score(y_pred=y_predict, y_true=Y_test))
